package ar.edu.itba.pod.legajo46470.cluster;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import org.apache.log4j.Logger;

import ar.edu.itba.node.Node;
import ar.edu.itba.node.NodeInformation;
import ar.edu.itba.node.api.ClusterAdministration;
import ar.edu.itba.pod.legajo46470.cluster.node.ClusterNode;

public class ClusterAdministrationImpl extends UnicastRemoteObject implements ClusterAdministration {
	private final static Logger LOGGER = Logger.getLogger(ClusterAdministration.class);
	private static final long serialVersionUID = -71483451066307792L;
	private Set<NodeInformation> connectedNodes;
	private ClusterNode clusterNode;
	private String groupId;
	
	public ClusterAdministrationImpl() throws RemoteException {
		super();
		this.connectedNodes = new CopyOnWriteArraySet<NodeInformation>();
	}

	@Override
	public void createGroup() throws RemoteException {		
		try {
			NodeInformation nodeInfo = clusterNode.getNodeInfo();
			
			// if not on a group, assign groupID with selfID
			if (!isConnectedToGroup()) {
				this.groupId = nodeInfo.id();
				addNewNode(nodeInfo);
				//clusterNode.getAgentsBalancer().bullyCoordinator(nodeInfo, DateTime.now().getMillis());
			} else {
				throw new IllegalStateException("Node " + clusterNode.getNodeInfo().id() + " already belongs to a cluster");
			}
		} catch (NotBoundException e) {
			LOGGER.error("Binding error, is rmi registry down?", e);
		}
	}

	@Override
	public String getGroupId() throws RemoteException {
		return this.groupId;
	}

	@Override
	public boolean isConnectedToGroup() throws RemoteException {
		return this.groupId != null;
	}

	@Override
	public void connectToGroup(String host, int port) throws RemoteException,
			NotBoundException {
		Set<NodeInformation> updatedNeighbours;
		
		try {
			ClusterAdministration remoteClusterAdmin = getStub(host, port);

			if (isConnectedToGroup()) {
				throw new IllegalStateException();
			}
			
			// Prevents self connection
			if (host.equals(clusterNode.getNodeInfo().host()) && port == clusterNode.getNodeInfo().port()) {
				throw new IllegalStateException("I cannot connect to my self!");
			}
			
			// prevents connecting to a node that doesn't have a group id
			if (!remoteClusterAdmin.isConnectedToGroup()) {
				throw new IllegalStateException("That node does not have a valid group id");
			}
			
			// establishes the groupId of the node i'm connecting to
			this.groupId = remoteClusterAdmin.getGroupId();
			
			// connects to the specified cluster (by adding self to that cluster)
			updatedNeighbours = remoteClusterAdmin.addNewNode(clusterNode.getNodeInfo());
			
			// updates known neighbours
			connectedNodes.add(clusterNode.getNodeInfo());
			if (updatedNeighbours != null) {
				// removes self from received list of neighbours
				updatedNeighbours.remove(clusterNode.getNodeInfo());
				connectedNodes.addAll(updatedNeighbours);
			}
						
		} catch (Exception e) {
			LOGGER.error("Fail to connect " + clusterNode.getNodeInfo().toString() + " to cluster: " + host + ":" + port);
		}
	}

	@Override
	public void disconnectFromGroup(NodeInformation node) throws RemoteException, NotBoundException {
		ClusterAdministration remoteClusterAdmin;
		if (!isConnectedToGroup()) {
			throw new IllegalStateException("Node " + clusterNode.getNodeInfo().id() + " is not connected to any group");
		}
				
		// self disconnection
		if (node.id().equals(clusterNode.getNodeInfo().id())) {
			// Removes self to prevent recursive disconnection
			if (connectedNodes.contains(node)) {
				connectedNodes.remove(node);
				for (NodeInformation n : connectedNodes) {
					if (!n.equals(clusterNode.getNodeInfo())) {
						try {
							remoteClusterAdmin = getStub(n.host(), n.port());
							remoteClusterAdmin.disconnectFromGroup(node);
						} catch (RemoteException e) {
							// do nothing
						} catch (NotBoundException e) {
							// do nothing
						}
					}
				}
				connectedNodes.clear();
			}
			groupId = null;
		} else {
			// if my cordinador is disconnecting, throw an election
			/*if (node.equals(clusterNode.getCoordinator())) {
				clusterNode.getAgentsBalancer().bullyElection(clusterNode.getNodeInfo(), DateTime.now().getMillis());
			}*/
			
			// recieved disconnection of someone else
			if (connectedNodes.contains(node)) {
				connectedNodes.remove(node);
				
				for (NodeInformation n : connectedNodes) {
					
					if (!n.equals(clusterNode.getNodeInfo())) {
						try {
							remoteClusterAdmin = getStub(n.host(), n.port());
							remoteClusterAdmin.disconnectFromGroup(node);
						} catch (RemoteException e) {
							// do nothing
						} catch (NotBoundException e) {
							// do nothing
						}
					}
				}
			}
		}
	}

	@Override
	public Set<NodeInformation> addNewNode(NodeInformation node)
			throws RemoteException, NotBoundException {
		ClusterAdministration remoteClusterAdmin;
		
		if (node == null) {
			throw new IllegalStateException("Null node recieved");
		}
		
		if (!isConnectedToGroup()) {
			throw new IllegalStateException("Node " + clusterNode.getNodeInfo().id() + " is not connected to any group");
		}
		
		// Verify groupId match
		LOGGER.info(node);
		remoteClusterAdmin = getStub(node.host(), node.port());
		// TODO ver si meter esta verificacion o no
		//if (!groupId.equals(remoteClusterAdmin.getGroupId())) {
		//	throw new IllegalStateException("Node " + node.id() + " groupId does not match Target Node " + clusterNode.getNodeInfo().id());
		//}
		
		
		if (!connectedNodes.contains(node)) {
			connectedNodes.add(node);
			
			// revisar si hay un algoritmo para broadcastear mejor msg de agregado
			// de un nodo
			for (NodeInformation n : connectedNodes) {
				if (!n.equals(clusterNode.getNodeInfo()) && !node.equals(n)) {
					remoteClusterAdmin = getStub(n.host(), n.port());
					remoteClusterAdmin.addNewNode(node);
					LOGGER.info("I: " + clusterNode.getNodeInfo() + " had informed: " + n + " of new neighbour: " + node);
				}
			}			
		}
		
		return connectedNodes;
	}

	@Override
	public Set<NodeInformation> connectedNodes() throws RemoteException {
		return this.connectedNodes;
	}

	public void setClusterNode(ClusterNode clusterNode) {
		this.clusterNode = clusterNode;
	}
	
	private ClusterAdministration getStub(String host, int port) throws RemoteException, NotBoundException {
		final Registry registry = LocateRegistry.getRegistry(host, port);
		ClusterAdministration clusterStub = (ClusterAdministration)
			registry.lookup(Node.CLUSTER_COMUNICATION);
		return clusterStub;
	}

}
